package cn.dansj.common.utils.shell;

import cn.dansj.common.utils.io.FileUtils;
import cn.dansj.common.utils.transfer.Verification;
import cn.dansj.common.utils.enums.GlobalEnv;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 功能描述: Shell命令运行工具类封装
 */
public class ShellUtils {
    private final Logger log;

    public ShellUtils(Logger log) {
        this.log = log;
    }

    public ShellUtils() {
        this.log = LoggerFactory.getLogger(ShellUtils.class);
    }

    Process process;

    boolean interrupt = false;

    private final StringBuilder finalResult = new StringBuilder();

    public String getFinalResult() {
        return finalResult.toString();
    }

    /**
     * @see #runShell(String[], Charset, String)
     */
    public int runShell(String[] cmd, Charset outputCharset) throws IOException, InterruptedException {
        return runShell(cmd, outputCharset, null);
    }

    public int runShell(String cmd) throws IOException, InterruptedException {
        if (isWindowsSystemOs()) {
            return runShell(new String[]{"cmd", "/c", cmd}, StandardCharsets.UTF_8, null);
        }
        return runShell(new String[]{"/bin/sh", "-c", cmd}, Charset.defaultCharset(), null);
    }

    public int runShell(String[] cmd, Charset outputCharset, String logFilePath) throws IOException, InterruptedException {
        long startTime = System.currentTimeMillis();
        boolean needReadProcessOutLogStreamByHand = true;
        log.info("current environment {}", ShellUtils.isWindowsSystemOs() ? "Windows" : "Linux");
        if (Verification.checkNotNull(logFilePath)) {
            log.info("receive new Command, cmd: {}, logFile:{}", String.join(" ", cmd), logFilePath);
        } else {
            log.info("receive new Command, cmd: {}", String.join(" ", cmd));
        }

        ProcessBuilder pb = new ProcessBuilder(cmd);
        initErrorLogHolder(logFilePath, outputCharset);
        int exitCode;
        try {
            if (logFilePath != null) {
                ensureFilePathExists(logFilePath);
                pb.redirectErrorStream(true);
                pb.redirectOutput(new File(logFilePath));
                needReadProcessOutLogStreamByHand = false;
            }
            process = pb.start();

            if (needReadProcessOutLogStreamByHand) {
                readProcessOutLogStream(process, outputCharset);
            }
            try {
                process.waitFor();
            } finally {
                try {
                    exitCode = process.exitValue();
                } catch (Exception exception) {
                    exitCode = -1;
                }
                log.info("process costTime:{}ms, exitCode:{}", System.currentTimeMillis() - startTime, exitCode);
            }
            if (exitCode != 0) {
                if (interrupt) {
                    throw new ShellProcessExecException(exitCode, new InterruptedException(GlobalEnv.InterruptedJobException));
                }

                String error = String.format("---进程返回异常信息, returnCode:%s, lastError:%s", exitCode, getProcessLastError());
                log.error(error);
            }
            return exitCode;
        } finally {
            removeErrorLogHolder();
        }
    }

    public void destroy(boolean interrupt) {
        this.interrupt = interrupt;
        if (process != null) {
            process.destroyForcibly();
            process = null;
        }
    }

    public int runShellWithRuntime(String cmd) throws IOException, InterruptedException {
        if (isWindowsSystemOs()) {
            return runShellWithRuntime(new String[]{"cmd", "/c", cmd}, Charset.defaultCharset());
        }
        return runShellWithRuntime(new String[]{"/bin/sh", "-c", cmd}, Charset.defaultCharset());
    }

    /**
     * 使用 Runtime.exec() 运行shell
     */
    public int runShellWithRuntime(String[] cmd, Charset outputCharset) throws IOException, InterruptedException {
        long startTime = System.currentTimeMillis();
        initErrorLogHolder(null, outputCharset);
        int exitCode;
        try {
            process = Runtime.getRuntime().exec(cmd);

            readProcessOutLogStream(process, outputCharset);
            try {
                process.waitFor();
            } finally {
                try {
                    exitCode = process.exitValue();
                } catch (Exception exception) {
                    exitCode = -1;
                }
                log.info("process costTime:{}ms, exitCode:{}", System.currentTimeMillis() - startTime, exitCode);
            }
            if (exitCode != 0) {
                if (interrupt) {
                    throw new ShellProcessExecException(exitCode, new InterruptedException(GlobalEnv.InterruptedJobException));
                }

                String error = String.format("进程返回异常信息, returnCode:%s, lastError:%s", exitCode, getProcessLastError());
                log.error(error);
            }
        } finally {
            removeErrorLogHolder();
        }
        return exitCode;
    }

    /**
     * 确保文件夹存在
     *
     * @param filePath 文件路径
     */
    public void ensureFilePathExists(String filePath) {
        File path = new File(filePath);
        if (path.exists()) {
            return;
        }
        File p = path.getParentFile();
        if (p.mkdirs()) {
            log.info("为文件创建目录: {} 成功", p.getPath());
            return;
        }
        log.warn("创建目录:{} 失败", p.getPath());
    }

    /**
     * 合并两个数组数据
     *
     * @param arrFirst  左边数组
     * @param arrAppend 要添加的数组
     * @return 合并后的数组
     */
    public String[] mergeTwoArr(String[] arrFirst, String[] arrAppend) {
        String[] merged = new String[arrFirst.length + arrAppend.length];
        System.arraycopy(arrFirst, 0, merged, 0, arrFirst.length);
        System.arraycopy(arrAppend, 0, merged, arrFirst.length, arrAppend.length);
        return merged;
    }

    /**
     * 删除以某字符结尾的字符
     *
     * @param originalStr 原始字符
     * @param toTrimChar  要检测的字
     * @return 裁剪后的字符串
     */
    public String trimEndsWith(String originalStr, char toTrimChar) {
        char[] value = originalStr.toCharArray();
        int i = value.length - 1;
        while (i > 0 && value[i] == toTrimChar) {
            i--;
        }
        return new String(value, 0, i + 1);
    }

    /**
     * 最后一次异常信息
     */
    private static final Map<Thread, ProcessErrorLogDescriptor> lastErrorHolder = new ConcurrentHashMap<>();

    /**
     * 主动读取进程的标准输出信息日志
     *
     * @param process       进程实体
     * @param outputCharset 日志字符集
     */
    private void readProcessOutLogStream(Process process, Charset outputCharset) {
        try (BufferedReader stdInput = new BufferedReader(new InputStreamReader(
                process.getInputStream(), outputCharset))) {
            Thread parentThread = Thread.currentThread();

            new Thread(() -> {
                try {
                    try (BufferedReader stdError = new BufferedReader(new InputStreamReader(process.getErrorStream(), outputCharset))) {
                        String err;
                        while ((err = stdError.readLine()) != null) {
                            log.error("{}", err);
                            setProcessLastError(parentThread, err);
                        }
                    }
                } catch (IOException e) {
                    log.error("读取进程错误日志输出时发生了异常", e);
                    setProcessLastError(parentThread, e.getMessage());
                }
            }).start();

            // 外部线程读取标准输出消息
            String stdOut;
            while ((stdOut = stdInput.readLine()) != null) {
                finalResult.setLength(0);
                finalResult.append(stdOut);
                if (Verification.checkNotNull(stdOut)) {
                    log.info("{}", stdOut);
                }
            }
        } catch (IOException ignore) {
        }
    }

    /**
     * 新建一个进程错误信息容器
     *
     * @param logFilePath 日志文件路径,如无则为 null
     */
    private void initErrorLogHolder(String logFilePath, Charset outputCharset) {
        lastErrorHolder.put(Thread.currentThread(), new ProcessErrorLogDescriptor(logFilePath, outputCharset));
    }

    /**
     * 移除错误日志监听
     */
    private void removeErrorLogHolder() {
        lastErrorHolder.remove(Thread.currentThread());
    }

    /**
     * 获取进程的最后错误信息
     * <p>
     * 注意: 该方法只会在父线程中调用
     */
    private String getProcessLastError() {
        Thread thread = Thread.currentThread();
        return lastErrorHolder.get(thread).getLastError();
    }

    /**
     * 设置最后一个错误信息描述
     * <p>
     * 使用当前线程或自定义
     */
    private void setProcessLastError(String lastError) {
        ProcessErrorLogDescriptor processErrorLogDescriptor = lastErrorHolder.get(Thread.currentThread());
        if (processErrorLogDescriptor != null) processErrorLogDescriptor.setLastError(lastError);
    }

    private void setProcessLastError(Thread thread, String lastError) {
        ProcessErrorLogDescriptor processErrorLogDescriptor = lastErrorHolder.get(thread);
        if (processErrorLogDescriptor != null) processErrorLogDescriptor.setLastError(lastError);
    }

    /**
     * 判断当前系统是否是 windows
     */
    public static boolean isWindowsSystemOs() {
        return System.getProperty("os.name").toLowerCase().startsWith("win");
    }

    /**
     * 进程错误信息描述封装类
     */
    private class ProcessErrorLogDescriptor {

        /**
         * 错误信息记录文件
         */
        private final String logFile;

        /**
         * 最后一行错误信息
         */
        private String lastError;
        private final Charset charset;

        ProcessErrorLogDescriptor(String logFile, Charset outputCharset) {
            this.logFile = logFile;
            charset = outputCharset;
        }

        String getLastError() {
            if (lastError != null) {
                return lastError;
            }
            try {
                if (logFile == null) {
                    return null;
                }
                List<String> lines = FileUtils.readLines(new File(logFile), String.valueOf(charset));
                StringBuilder sb = new StringBuilder();
                for (int i = lines.size() - 1; i >= 0; i--) {
                    sb.insert(0, lines.get(i) + "\n");
                    if (sb.length() > 200) {
                        break;
                    }
                }
                return sb.toString();
            } catch (Exception e) {
                log.error("读取最后一次错误信息失败", e);
            }
            return null;
        }

        void setLastError(String err) {
            if (lastError == null) {
                lastError = err;
                return;
            }
            lastError = lastError + "\n" + err;
            if (lastError.length() > 200) {
                lastError = lastError.substring(lastError.length() - 200);
            }
        }
    }
}